/*
	particlesystem.cpp

  Copyright (C) 2004 WildTangent, Inc. 
  All Rights Reserved
  
	Travis Baldree
	2/26/2004
	
*/

#include <assert.h>

#include <d3d9.h>
#include <d3dx9.h>

#include "../UTILITIES/macros.h"
#include "../UTILITIES/constants.h"
#include "../UTILITIES/llist.h"
#include "../UTILITIES/math3d.h"
#include "../UTILITIES/settings.h"
#include "../UTILITIES/vertexdefinitions.h"
#include "../UTILITIES/frustum.h"
#include "particlesystem.h"

CParticleSystem::CParticleSystem( LPDIRECT3DDEVICE9 pD3DDevice,		// our direct3d device
								  CSettings& pSettings,				// settings object, to let us know what we can do
								  uint32 MaximumParticles,			// maximum particles the system can hold
								  float32 ParticleWidthHeight,		// width and height of basic particles, in units
								  const D3DXVECTOR3& Gravity,		// Direction and speed of fall, in units per second
								  float32 ScaleSpeed,				// change in scale in units per second
								  float32 FadeSpeed,				// change in opacity per second - 0 means no fading - opacity is 0-255, so a value of -255 would be totally transparent in 1 second
								  uint32 FlipbookWidthHeight,		// if this is an animated particle, how many frames wide/high is our animation sheet
								  float32 SecondsPerFlipbookFrame,  // how much time is spent on each frame of a flipbook animation
								  bool Multiply,					// multiply blend?
								  bool DoubleSided ) :				// double sided sprite?
														m_pSettings( pSettings ),
														m_MaximumParticles( MaximumParticles ),
														m_ParticleWidthHeight( ParticleWidthHeight ),
														m_Gravity( Gravity ),
														m_ScaleSpeed( ScaleSpeed ),
														m_FadeSpeed( FadeSpeed ),
														m_FlipbookWidthHeight( FlipbookWidthHeight ),
														m_FlipbookFrames( FlipbookWidthHeight * FlipbookWidthHeight ),
														m_SecondsPerFlipbookFrame( SecondsPerFlipbookFrame ),
														m_IsAnimated( FlipbookWidthHeight > 1 ),
														m_Multiply( Multiply ),
														m_HasGravity( kTrue ),
														m_SystemValid( kFalse ),
														m_AdditiveFading( kFalse ),
														m_Directed( kFalse ),
														m_DoubleSided( DoubleSided ),
														m_FlipbookFrameUVWidth( 1 ),
														m_ActiveParticles( 0 ),
														m_pVB( NULL ),
														m_pParticleList( new CLList<CParticle>() )
{
	if( D3DXVec3Length( &m_Gravity ) > 0 )
	{
		m_HasGravity = kTrue;
	}
	
	if( m_FlipbookFrames > 1 )
	{
		m_FlipbookFrameUVWidth = (float32)( 1.0f / m_FlipbookWidthHeight );
	}

	m_SystemValid = CreateVB( pD3DDevice );
} // CParticleSystem::CParticleSystem()



CParticleSystem::~CParticleSystem( void )
{
	RELEASE_SAFELY( m_pVB );
	DELETE_SAFELY( m_pParticleList );
} // CParticleSystem::~CParticleSystem()

void CParticleSystem::Add( CParticle* pParticle )	// particle to add
{
	assert( pParticle != NULL );

	if( !m_SystemValid )
	{
		return;
	}

	// if it won't exceed our maximum, add this particle to the list
	if( m_ActiveParticles < m_MaximumParticles )
	{
		m_pParticleList->Queue( pParticle );

		if( m_HasGravity )
		{
			pParticle->m_IsMoving = kTrue;
		}
	}	
} // CParticleSystem::Add()

void CParticleSystem::Clear( void )
{
	// remove all particles from the linked list
	m_pParticleList->Destroy();
} // CParticleSystem::Clear()

bool CParticleSystem::CreateVB( LPDIRECT3DDEVICE9 pD3DDevice )	// our direct3d device
{
	HRESULT hr;
	uint32 Verts( 4 );
	if( m_DoubleSided )
	{
		Verts = 6;
	}

	// set up the vert buffer for all sprites
    hr = pD3DDevice->CreateVertexBuffer( Verts * sizeof(SPRITEVERTEX),
										 D3DUSAGE_WRITEONLY, 
										 D3DFVF_SPRITEVERTEX,
										 D3DPOOL_MANAGED, 
										 &m_pVB,
										 NULL );
	
	if( FAILED(hr) )
	{
		return kFalse;
	}

	float32 HalfWidth = m_ParticleWidthHeight * .5f;

	SPRITEVERTEX* pVertices;
	if( FAILED( m_pVB->Lock( 0, 0, (void**)&pVertices, NULL ) ) )
	{
		return kFalse;
	}

	pVertices[0].v.x	= -HalfWidth;
	pVertices[0].v.y	= HalfWidth;
	pVertices[0].v.z	= 0;
	pVertices[0].tu   = 0;
	pVertices[0].tv   = 0;
	pVertices[0].color	= D3DXCOLOR( 1.0f, 1.0f, 1.0f, 1.0f );

	pVertices[1].v.x	= HalfWidth;
	pVertices[1].v.y	= HalfWidth;
	pVertices[1].v.z	= 0;
	pVertices[1].tu   = m_FlipbookFrameUVWidth;
	pVertices[1].tv   = 0;
	pVertices[1].color	= D3DXCOLOR( 1.0f, 1.0f, 1.0f, 1.0f );

	pVertices[2].v.x	= HalfWidth;
	pVertices[2].v.y	= -HalfWidth;
	pVertices[2].v.z	= 0;
	pVertices[2].tu   = m_FlipbookFrameUVWidth;
	pVertices[2].tv   = m_FlipbookFrameUVWidth;
	pVertices[2].color	= D3DXCOLOR( 1.0f, 1.0f, 1.0f, 1.0f );

	pVertices[3].v.x	= -HalfWidth;
	pVertices[3].v.y	= -HalfWidth;
	pVertices[3].v.z	= 0;
	pVertices[3].tu   = 0;
	pVertices[3].tv   = m_FlipbookFrameUVWidth;
	pVertices[3].color	= D3DXCOLOR( 1.0f, 1.0f, 1.0f, 1.0f );

	if( m_DoubleSided )
	{
		pVertices[4].v.x	= HalfWidth;
		pVertices[4].v.y	= -HalfWidth;
		pVertices[4].v.z	= 0;
		pVertices[4].tu   = m_FlipbookFrameUVWidth;
		pVertices[4].tv   = m_FlipbookFrameUVWidth;
		pVertices[4].color	= D3DXCOLOR( 1.0f, 1.0f, 1.0f, 1.0f );

		pVertices[5].v.x	= HalfWidth;
		pVertices[5].v.y	= HalfWidth;
		pVertices[5].v.z	= 0;
		pVertices[5].tu   = m_FlipbookFrameUVWidth;
		pVertices[5].tv   = 0;
		pVertices[5].color	= D3DXCOLOR( 1.0f, 1.0f, 1.0f, 1.0f );
	}

	m_pVB->Unlock();	

	return kTrue;
} // CParticleSystem::CreateVB()

void CParticleSystem::Update( float32 TimeElapsed )					// time elapsed in seconds
{
	if( !m_SystemValid )
	{
		return;
	}

	// we're going to re-count our particles, so reset our tally
	m_ActiveParticles = 0;

	CLLNode<CParticle>* pCurrentParticle	= m_pParticleList->GetFirst();
	CLLNode<CParticle>* NextParticle	= NULL;
	CParticle*	pParticleData;

	float32 FadeChange		= m_FadeSpeed * TimeElapsed;
	float32 FrameIncrement	= TimeElapsed / m_SecondsPerFlipbookFrame;
	float32 FrameGate		= (float32)m_FlipbookFrames - .5f;

	bool CanFade( kFalse );
	if( m_FadeSpeed != 0 && m_pSettings.GetSettings( KSetAlphaFade ) )
	{
		CanFade = kTrue;
	}

	while( pCurrentParticle != NULL )
	{
		bool RemoveParticle = kFalse;

		m_ActiveParticles++;

		NextParticle = pCurrentParticle->Next();
		pParticleData = pCurrentParticle->Data();

		pParticleData->m_Opacity += FadeChange;

		if( pParticleData->m_Opacity > 255 )
		{
			pParticleData->m_Opacity = 255;
		}
		// invisible particles should be removed
		if( pParticleData->m_Opacity < 0 ||
			( !CanFade && pParticleData->m_Opacity < 75 ) )
		{
			pParticleData->m_Opacity = 0;
			RemoveParticle = kTrue;
		}

		// if particles are flipbook animated
		if( m_FlipbookFrames > 1 )
		{
			pParticleData->m_FlipbookFrame	+= FrameIncrement;
			if( pParticleData->m_FlipbookFrame >= FrameGate )
			{
				pParticleData->m_AnimationLoops++;
				pParticleData->m_FlipbookFrame -= (float32)m_FlipbookFrames;
				// if this particle disappears after a certain number of animation loops
				if( pParticleData->m_AnimationCycleLifetime > 0 )
				{
					if( pParticleData->m_AnimationLoops >= 
						pParticleData->m_AnimationCycleLifetime )
					{
						RemoveParticle = kTrue;
					}
				}
			}
		}

		// rotate around the z axis
		pParticleData->m_ZRotation += pParticleData->m_ZRotationSpeed * TimeElapsed;

		pParticleData->m_LifeTime += TimeElapsed;
		// if this particle doesn't expire via animation loops, and it's exceeded
		// the max life time, we remove it
		if( pParticleData->m_AnimationCycleLifetime == 0 &&
			pParticleData->m_LifeTime >= pParticleData->m_MaxLifeTime )
		{
			RemoveParticle = kTrue;
		}

		if( pParticleData->m_IsMoving )
		{
			if( m_HasGravity )
			{
				pParticleData->m_Velocity += m_Gravity * TimeElapsed;
			}
			pParticleData->m_Position += pParticleData->m_Velocity * TimeElapsed;
		}

		pParticleData->m_Scale += m_ScaleSpeed * TimeElapsed;
		// prevent particles from becoming too small
		if( pParticleData->m_Scale < .001 )
		{
			pParticleData->m_Scale = .001f;
		}
		if( RemoveParticle )
		{
			m_pParticleList->Remove( pCurrentParticle );
		}
		pCurrentParticle = NextParticle;
	}
} // CParticleSystem::Update()

void CParticleSystem::Render( LPDIRECT3DDEVICE9 pD3DDevice,			// our direct3d device
							  const D3DXMATRIX& ViewportMatrix,		// viewport matrix
							  CFrustum*	pFrustum )					// frustum to cull against (NULL for no culling)

{
	if( !m_SystemValid )
	{
		return;
	}

	D3DXMATRIX UVTransformation;
	D3DXMatrixIdentity(&UVTransformation);

	D3DXMATRIX Translation;
	D3DXMATRIX Scale;
	D3DXMATRIX Rotation;

	D3DXVECTOR3 Start( 0, 0, 0 );
	D3DXVECTOR3 Up( 0, 1, 0 );
	D3DXVECTOR3 Forward( 0, 0, 1 );

	D3DXMATRIX Orientation;
	D3DXMatrixTranspose( &Orientation, &ViewportMatrix);
	Orientation._41=0;
	Orientation._42=0;
	Orientation._43=0;
	Orientation._14=0;
	Orientation._24=0;
	Orientation._34=0;


	
    pD3DDevice->SetFVF( D3DFVF_SPRITEVERTEX );
    pD3DDevice->SetStreamSource( 0, m_pVB, 0, sizeof(SPRITEVERTEX) );

	CLLNode<CParticle>* pCurrentParticle	= m_pParticleList->GetFirst();
	CParticle*			pParticleData	= NULL;

	int32	LastSetFrame	= -1;
	uint32	Fade			= 0;
	int32	Frame			= 0;
	int32	Row				= 0;
	int32	Col				= 0;
	bool	CanFade			= kFalse;
	bool	IsScaling		= kFalse;

	// we only fade if this particle system has animated opacity and the machine supports it
	// expensive stuff, so be careful!
	if( m_FadeSpeed != 0 && m_pSettings.GetSettings( KSetAlphaFade ) )
	{
		CanFade = kTrue;

		// turn on fading
		pD3DDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_BLENDFACTORALPHA );
		if( m_AdditiveFading )
		{
			pD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_TFACTOR );
		}
		else
		{
			pD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_TEXTURE );
			pD3DDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG2, D3DTA_TFACTOR );
		}
	}

	if( m_ScaleSpeed != 0 )
	{
		IsScaling = kTrue;
	}

	// if we are animated, set up a texture transformation
	if( m_FlipbookFrames > 1)
	{
		pD3DDevice->SetTextureStageState( 0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_COUNT2 );
	}

	while( pCurrentParticle != NULL )
	{
		pParticleData = ( CParticle* )pCurrentParticle->Data();

		if( pFrustum == NULL || 
			pFrustum->SphereIntersectsAABB( pParticleData->m_Position, 
											m_ParticleWidthHeight * pParticleData->m_Scale ) )
		{
			// if this particle system has a flipbook animation
			if( m_FlipbookFrames > 1 )
			{
				Frame = (uint32)floor( pParticleData->m_FlipbookFrame + .5f );

				// no sense changing the UV transform if the frame is the same
				if( Frame != LastSetFrame )
				{
					Row	=	(uint32)floor( (float32)Frame / (float32)m_FlipbookWidthHeight );
					Col	=	Frame - ( ( Row ) * m_FlipbookWidthHeight );
					UVTransformation._31 = Col * m_FlipbookFrameUVWidth;
					UVTransformation._32 = Row * m_FlipbookFrameUVWidth;
					pD3DDevice->SetTransform( D3DTS_TEXTURE0, &UVTransformation );

					LastSetFrame = Frame;
				}
			}

			if( CanFade )
			{
				uint32 Fade = (uint32)floor( pParticleData->m_Opacity + .5f );
				if( m_Multiply )
				{
					pD3DDevice->SetRenderState( D3DRS_TEXTUREFACTOR, 
												D3DCOLOR_RGBA( 255, 255, 255, Fade ) );			
				}
				else
				{
					pD3DDevice->SetRenderState( D3DRS_TEXTUREFACTOR, 
												D3DCOLOR_RGBA( 0, 0, 0, Fade) );			
				}
			}

			D3DXMatrixTranslation( &Translation, 
								   pParticleData->m_Position.x,
								   pParticleData->m_Position.y,
								   pParticleData->m_Position.z );

			D3DXMatrixScaling( &Scale,
							   pParticleData->m_Scale,
							   pParticleData->m_Scale,
							   0 );


			D3DXMatrixRotationZ( &Rotation, 
								 (float32)( pParticleData->m_ZRotation * KDeg2Rad ) );

			if( m_Directed )
			{
				Forward = pParticleData->m_Velocity;
				D3DXVec3Normalize( &Forward, &Forward );
				if( fabs( pParticleData->m_Velocity.x ) < .001f &&
					fabs( pParticleData->m_Velocity.z ) < .001f )
				{
					Up.x = 0;
					Up.y = 0;
					Up.z = 1;
				}
				else
				{
					Up.x = 0;
					Up.y = 1;
					Up.z = 0;
				}
				D3DXVec3Normalize( &Up, &Up );
				D3DXMatrixLookAtLH( &Orientation, &Start, &Forward, &Up );
				D3DXMatrixTranspose( &Orientation, &Orientation );
			}

			Translation = Scale * Rotation * Orientation *
						  Translation *
						  ViewportMatrix;

			pD3DDevice->SetTransform( D3DTS_VIEW, &Translation );
			
			if( m_DoubleSided )
			{
				pD3DDevice->DrawPrimitive( D3DPT_TRIANGLEFAN,  0, 4 );
			}
			else
			{
				pD3DDevice->DrawPrimitive( D3DPT_TRIANGLEFAN,  0, 2 );
			}
		}
		else	// not in the frustum, so we mark this particle for 
				// immediate removal
		{
			pParticleData->m_Opacity = -1;
		}
		pCurrentParticle = pCurrentParticle->Next();
	}
	// if we are animated, turn off the texture transformation
	if( m_FlipbookFrames > 1)
	{
		pD3DDevice->SetTextureStageState( 0, D3DTSS_TEXTURETRANSFORMFLAGS, D3DTTFF_DISABLE );
	}

	// turn off fading
	if( CanFade )
	{
		pD3DDevice->SetTextureStageState( 0, D3DTSS_COLOROP, D3DTOP_MODULATE );
		if( m_AdditiveFading )
		{
			pD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_CURRENT );
		}
		else
		{
			pD3DDevice->SetTextureStageState( 0, D3DTSS_COLORARG2, D3DTA_CURRENT );
			pD3DDevice->SetTextureStageState( 0, D3DTSS_ALPHAARG2, D3DTA_CURRENT );
		}
	}

} // CParticleSystem::Render()



